Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | import nextDynamic from 'next/dynamic' import { notFound, redirect } from 'next/navigation' import { canPerformAction } from '@/lib/classroom/access-control' import { getActiveSessionPlan, getPracticeStudent } from '@/lib/curriculum/server' import { isEnabled } from '@/lib/feature-flags' import { getEffectiveTierForStudent } from '@/lib/subscription' import { getUserId } from '@/lib/viewer' // Skip SSR for PracticeClient — practice is fully interactive and has hooks // (useHasPhysicalKeyboard, useSearchParams) that produce server/client mismatches const PracticeClient = nextDynamic(() => import('./PracticeClient').then((m) => m.PracticeClient), { ssr: false, }) // Disable caching for this page - session state must always be fresh export const dynamic = 'force-dynamic' interface StudentPracticePageProps { params: Promise<{ studentId: string }> } /** * Student Practice Page - Server Component * * This page ONLY shows the current problem for active practice sessions. * All other states redirect to appropriate pages. * * Guards/Redirects: * - No active session → /dashboard (show progress, start new session) * - Draft/approved session (not started) → /dashboard (modal handles configuration) * - In_progress session → SHOW PROBLEM (this is the only state we render here) * - Completed session → /summary (show results) * * URL: /practice/[studentId] */ export default async function StudentPracticePage({ params }: StudentPracticePageProps) { const { studentId } = await params // Fetch player and active session in parallel const [player, activeSession] = await Promise.all([ getPracticeStudent(studentId), getActiveSessionPlan(studentId), ]) // 404 if player doesn't exist or is not a practice student if (!player) { notFound() } // Check authorization - user must have view access to this player const viewerId = await getUserId() const hasAccess = await canPerformAction(viewerId, studentId, 'view') if (!hasAccess) { notFound() // Return 404 to avoid leaking existence of player } // No active session → dashboard if (!activeSession) { redirect(`/practice/${studentId}/dashboard`) } // Draft or approved but not started → dashboard (modal handles configuration) if (!activeSession.startedAt) { redirect(`/practice/${studentId}/dashboard`) } // Session is completed → summary page if (activeSession.completedAt) { redirect(`/practice/${studentId}/summary`) } // Check if session songs are enabled for this student const [songFlagEnabled, tierResult] = await Promise.all([ isEnabled('session-song.enabled'), getEffectiveTierForStudent(studentId, viewerId), ]) const songEnabled = songFlagEnabled && tierResult.tier === 'family' // Only state left: in_progress session → show problem return ( <PracticeClient studentId={studentId} player={player} initialSession={activeSession} songEnabled={songEnabled} /> ) } |